package com.vinjogames.entities;

import java.io.InputStream;
import java.io.Serializable;
import java.nio.FloatBuffer;
import java.util.ArrayList;

import javax.microedition.khronos.opengles.GL10;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.PointF;
import android.util.FloatMath;

import com.vinjogames.gfx.Image;
import com.vinjogames.gfx.SpriteSheet;
import com.vinjogames.util.GameConVars;
import com.vinjogames.util.Logger;
import com.vinjogames.util.PointFFlagged;
import com.vinjogames.util.VertexBuffer;


/**
 * GameMap:
 * 	 This class will load in a preset height map along with coordinates for resource nodes,
 * player bases, and neutral turret locations.
 * 
 * 
 * @author Joey
 *
 */
public class GameMap implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 8048185589609814392L;

	public static AttackObject World = new Unit();

	private int index = 0;
	private static int p = 0;

	private float size; 

	private Game mGame;
	String mapName;
	private int tileset;
	private PointF[] turretLocations;
	private PointF[] resourceLocations;
	private PointF[] foliageLocations;
	private PointF[] baseLocations;
	private PointFFlagged[] heightMap;
	private PointF[] deboorPoints;
	private PointFFlagged[] drawPoints;
	private PointF[] craters;
	
	private MapBuildPoint[] clampPoints;
	private ArrayList<MapBuildPoint> structurePoints;

	private int turretCount;
	private int resourceCount;
	private int mapPoints;

	private Image[] foliage;
	private Image craterTexture;

	private float mMapMaskVerts[];
	private float mDirtTextureScale = 1.5f; //Texture zooming (repeating factor) ...affects fps a lot
	private int[] mDirtTexture;
	private FloatBuffer mMaskVertBuffer;
	private FloatBuffer mWorldVertBuffer;
	private FloatBuffer mGrassVertBuffer;
	private FloatBuffer mDirtTextureBuffer;

	public GameMap(Game game) {
		mGame = game;
		craters = new PointFFlagged[GameConVars.MAP_MAX_CRATERS];
		foliage = new Image[GameConVars.MAP_MAX_FOLIAGE];
		index = 0;		
	}

	public static GameMap getDefaultMap() {
		//TODO: Make the default map some sort of really simple 2 player map (perhaps for test purposes make it 100% flat with like 1 resource, 2 turrets, and 2 bases)
		return new GameMap(null);
	}	
	
	public String loadMapFromFile(Activity act, int file) {
		String map = null;
		try{
			Resources res = act.getResources();
			InputStream in = res.openRawResource(file);

			byte[] buffer = new byte[in.available()];
			in.read(buffer);
			map = (new String(buffer));
			in.close();
		} 
		catch(Exception e){
			Logger.log("Exception e: " + e.getMessage());
		}
		return map;
	}

	public boolean loadMapInfo(String mapInfo) {
		//TODO: generate the neutral turrets and resources along with player bases.
		parseMapFromJson(mapInfo);

		generateHeightMap();
		//clampDrawPoints();
		updateMapDisplay();
		
		parseFoliage(mapInfo);
		
		return false;
	}
	
	/**
	 * Parses all the info from the json file, filling in the heightmap, 
	 * and locations of game objects
	 * @param gameMapFile The actual json string that contains all the info
	 */
	public void parseMapFromJson(String gameMapFile){
		try {
			JSONObject map = new JSONObject(gameMapFile);
			mapName = (String)map.get("Name");
			tileset = map.getInt("Tileset");
			this.size = (Integer)map.get("Size");

			//Parses map points
			JSONArray points = map.getJSONArray("Points");
			
			initMapArrays(points.length());
			
			for (int i = 0; i < points.length(); ++i) {
				JSONObject point = points.getJSONObject(i);
				int x = (Integer)point.get("x");
				int y = (Integer)point.get("y");

				heightMap[i].set(x,y);
				heightMap[i].frozen = false;
			}

			//Parses turret info
			JSONArray turrets = map.getJSONArray("Turrets");

			this.turretCount = turrets.length();
			this.structurePoints = new ArrayList<MapBuildPoint>();
			this.turretLocations = new PointF[turretCount];

			for (int i = 0; i < turrets.length(); i++) {
				JSONObject turret = turrets.getJSONObject(i);
				int x = (Integer)turret.get("x");
				int y = (Integer)turret.get("y");
				
				freezeMapPoints(x, GameConVars.MAP_TURRET_WIDTH);

				turretLocations[i] = new PointF(x,y);
				structurePoints.add(new MapBuildPoint(x, y, GameConVars.MAP_POINT_TYPE_TURRET));
				Logger.log("MAP", "Put turret at (" + x + "," + y + ")");
			}

			//Parses  base locations
			JSONArray bases = map.getJSONArray("Bases");
			baseLocations = new PointF[bases.length()];
			for (int i = 0; i < bases.length(); i++) {
				JSONObject base = bases.getJSONObject(i);
				String owner = (String)base.get("Owner");
				int x = (Integer)base.get("x");
				int y = (Integer)base.get("y");
				
				freezeMapPoints(x, GameConVars.MAP_BASE_WIDTH);
				
				structurePoints.add(new MapBuildPoint(x, y, GameConVars.MAP_POINT_TYPE_BASE));
				Logger.log("MAP", "Put base at (" + x + "," + y + ")");
				//TODO::
				Logger.log("Base locs: " + owner + ":"+ x + ":" + y);
				baseLocations[i] = new PointF(x,y);
			}

			//Parses resource info
			JSONArray resources = map.getJSONArray("Resources");
			this.resourceCount = resources.length();
			
			this.resourceLocations = new PointF[resourceCount];
			for (int i = 0; i < resourceCount; i++) {
				JSONObject resource = resources.getJSONObject(i);
				int x = (Integer)resource.get("x");
				int y = (Integer)resource.get("y");
				
				freezeMapPoints(x, GameConVars.MAP_RESOURCE_WIDTH);
				
				resourceLocations[i] = new PointF(x,y);
				structurePoints.add(new MapBuildPoint(x, y, GameConVars.MAP_POINT_TYPE_RESOURCE));
				Logger.log("MAP", "Put resource at (" + x + "," + y + ")");
			}

//			//Parses foliage info
//			JSONArray foliages = map.getJSONArray("Foliage");
//			foliageLocations = new PointF[foliages.length()];
//			for (int i = 0; i < foliages.length(); i++) {
//				JSONObject foliage = foliages.getJSONObject(i);
//				String image = (String)foliage.get("Image");
//				float x = (float)foliage.getDouble("x");
//				float y = (float)drawPoints[getClosestPoint((int)x)].y;
////				float y = (float)foliage.getDouble("y");
//				foliageLocations[i] = new PointF(x,y);
//			}
			
			clampPoints = structurePoints.toArray(new MapBuildPoint[0]);

		} catch (JSONException e) {
			Logger.e(e);
		}
	}
	
	public void parseFoliage(String gameMapFile){
		JSONObject map;
		try {
			map = new JSONObject(gameMapFile);
			
			//Parses foliage info
			JSONArray foliages = map.getJSONArray("Foliage");
			foliageLocations = new PointF[foliages.length()];
			for (int i = 0; i < foliages.length(); i++) {
				JSONObject foliage = foliages.getJSONObject(i);
				String image = (String)foliage.get("Image");
				float x = (float)foliage.getDouble("x");
				float y = (float)drawPoints[getClosestPoint((int)x)].y;
//				float y = (float)foliage.getDouble("y");
				foliageLocations[i] = new PointF(x,y);
			}
		} catch (JSONException e) {
			// TODO Auto-generated catch block
			Logger.e(e);
		}
	}

	public void init(String map){
		SpriteSheet ss = SpriteSheet.getInstance();
		loadMapInfo(map);
		
		mDirtTexture = ss.getTexture(GameConVars.GRAPHIC_GROUND_TEXTURES[tileset]);
		foliage[0] = new Image(GameConVars.GRAPHIC_TREE,1,1);//Move foliage width/height somewhere


	}

	/**
	 * 
	 * Gets the closest points to tanks to help with rotating the tank as it drives up and down hills.
	 * 
	 * @param p A point.
	 * @return An index?!
	 * 
	 */
	public int getClosestPoint(float x) {
		int min = 0;
		int max  = drawPoints.length - 1;

		while (min < max) {
			int mid = (min + max) / 2;  
			if (x < drawPoints[mid].getX()) {
				max = mid;    
			} else if (x > drawPoints[mid].getX()) {
				min = mid + 1;  
			} else {
				return mid;     
			}
		}
		return (min);
	}
	
	public void mapDamageOccurred() {
		generateHeightMap();
		//clampDrawPoints();
		updateMapDisplay(); //TODO:  optimize, make sure its not doing too much
		// TODO Move Falling Stuff From Units Here...   Blargity...
		if(this != null)
			for(Unit u : mGame.getUnits())
				if(u != null)
					u.floating(this);
	}

	/**
	 * Helper function for generateMap(). Adds a new point to the list of drawn points.
	 * @param x The x location of the point.
	 * @param y The y location of the point.
	 */
	private void addPoint(float x, float y) {
		if(index >= drawPoints.length) return;
		drawPoints[index].setX(x);
		drawPoints[index++].setY(y);
	}
	
	

	/**
	 * Generates a height map and all the intermediate points using De'Boor's Algorithm.
	 */
	private void generateHeightMap() {
		index = 0;
		//Add the first control point of the map to the start of the curve.
		addPoint(heightMap[0].getX(), heightMap[0].getY());

		//Iterate along the curve generating points using DeBoor's algorithm.
		for(float t = (float)GameConVars.MAP_DEGREE; t < mapPoints && index < drawPoints.length - 1; t += (1.0f / GameConVars.MAP_DETAIL_LEVEL))	{
			for(int i = 0; i < mapPoints; ++i) {
				deboorPoints[i].x = heightMap[i].getX();
				deboorPoints[i].y = heightMap[i].getY();
			}

			int j = (int)FloatMath.floor(t);

			for(p = 1; p <= GameConVars.MAP_DEGREE; ++p) {
				for(int i = j - GameConVars.MAP_DEGREE + p; i <= j; ++i)
				{
					deboorPoints[(p * mapPoints)+i].x =
							(float)((t - i) /
									(	(GameConVars.MAP_DEGREE - p + 1))) *
									deboorPoints[((p - 1)* mapPoints)+i].x 

									+

									(float)((	(i + GameConVars.MAP_DEGREE - (p - 1))	 - t) /
											(	(GameConVars.MAP_DEGREE - p + 1))) *
											deboorPoints[((p - 1)* mapPoints)+(i-1)].x;


					deboorPoints[(p * mapPoints)+i].y =
							(float)((t - i) /
									(	(GameConVars.MAP_DEGREE - p + 1))) * 
									deboorPoints[((p - 1)* mapPoints)+i].y 

									+

									(float)((	(i + GameConVars.MAP_DEGREE - (p - 1))	 - t) /
											(	(GameConVars.MAP_DEGREE - p + 1))) *
											deboorPoints[((p - 1)* mapPoints)+(i-1)].y;
				}
			}
			j = GameConVars.MAP_DEGREE * mapPoints + j;
			addPoint(deboorPoints[j].x, deboorPoints[j].y);
		}
		//Clamp the end of the curve onto the final control point.
		addPoint(heightMap[mapPoints - 1].getX(), heightMap[mapPoints - 1].getY());
	}
	
	
	/**
	 * @param xLoc
	 * @param power
	 */
	public void damageMap(float xLoc, float power) {
		if(xLoc <= 0 || xLoc >= size) return;
		
		int loopVar = 0;
		while(loopVar < heightMap.length - 2 && !(xLoc > heightMap[loopVar].x && xLoc < heightMap[loopVar+1].x)) {
			++loopVar;
		}

		//Drop the y values of the end points for the line segment that was hit.
		//Use the formulas y = y - power * (x - start)/(end-start) and (end - x)/(end-start)
		if(!heightMap[loopVar].frozen) heightMap[loopVar].y -= power * Math.abs((xLoc- heightMap[loopVar].x) / (heightMap[loopVar+1].x - heightMap[loopVar].x));
		if(!heightMap[loopVar+1].frozen) heightMap[loopVar+1].y -= power * Math.abs((heightMap[loopVar+1].x - xLoc) / (heightMap[loopVar+1].x - heightMap[loopVar].x));
		
		//Do not let the map fall below 0.
		if(heightMap[loopVar].y < 0.0f) {
			heightMap[loopVar].y = 0.0f;
			heightMap[loopVar].frozen = true;
		}
		if(heightMap[loopVar+1].y < 0.0f) {
			heightMap[loopVar+1].y = 0.0f;
			heightMap[loopVar+1].frozen = true;
		}
	}
	
	

	public void draw(GL10 gl){	
		gl.glPushMatrix();		

		//Draws the map to the stencil buffer as a mask
		gl.glEnable(GL10.GL_STENCIL_TEST);
		gl.glClearStencil(0);
		gl.glClear(GL10.GL_STENCIL_BUFFER_BIT);
		gl.glColorMask(false, false, false, false);
		gl.glDepthMask(false);
		gl.glDisable(GL10.GL_DEPTH_TEST); 
		gl.glStencilFunc(GL10.GL_ALWAYS, 1, 1);
		gl.glStencilOp(GL10.GL_REPLACE, GL10.GL_REPLACE, GL10.GL_REPLACE);

		gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mMaskVertBuffer);
		gl.glColor4f(1.0f, 0.0f, 1.0f, 1.0f);
		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, mMapMaskVerts.length/3);


		gl.glColorMask(true, true, true, true);
		gl.glDepthMask(true);
		gl.glStencilFunc(GL10.GL_EQUAL, 1, 1);
		gl.glStencilOp(GL10.GL_KEEP, GL10.GL_KEEP, GL10.GL_KEEP);

		// Bind Texture
		gl.glBindTexture(GL10.GL_TEXTURE_2D, mDirtTexture[0]);
		// Point to Buffers
		gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		// Set Face
		gl.glFrontFace(GL10.GL_CW);
		// Point to buffer
		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mWorldVertBuffer);
		gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mDirtTextureBuffer);
		gl.glColor4f(1f, 1f, 1f, 1f);		
		gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
		//Draws over the whole map where masked by stencil
		gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glDisable(GL10.GL_STENCIL_TEST);

		//Draw green line (grass)
//		//TODO:  change to grass texture, drawn over a line if possible
//		gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mGrassVertBuffer);
//		gl.glColor4f(0, 1f, 0, 1f);		
//		gl.glLineWidth(15f);//TODO:  this doesn't respect scale
//		gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, mMapMaskVerts.length/3/2);
//		gl.glLineWidth(1f);

		// Disable Client State before Leaving
		gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
		gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
		gl.glColor4f(1f, 1f, 1f, 1f);
		gl.glPopMatrix();

		for(int i = 0; i < foliageLocations.length; i++){
			if(foliageLocations[i].y == (float) drawPoints[getClosestPoint(foliageLocations[i].x)].y)
				foliage[0].draw(gl, foliageLocations[i]);
		}
	}
	
	private void initMapArrays(int mapLength) {
		
		mapPoints = mapLength;
		
		heightMap = new PointFFlagged[mapPoints];
		for(int i = 0; i < heightMap.length; ++i)
			heightMap[i] = new PointFFlagged();

		deboorPoints = new PointFFlagged[(GameConVars.MAP_DEGREE + 1) * mapPoints];
		for(int i = 0; i < deboorPoints.length; ++i)
			deboorPoints[i] = new PointFFlagged();

		drawPoints = new PointFFlagged[(mapPoints - GameConVars.MAP_DEGREE) * GameConVars.MAP_DETAIL_LEVEL + 2];
		for(int i = 0; i < drawPoints.length; ++i) {
			drawPoints[i] = new PointFFlagged();
		}
	}

	/**
	 * Call this when drawPoints is changed to update
	 * map display
	 */
	private void updateMapDisplay(){
		mMapMaskVerts = new float[drawPoints.length*3*2];
		for(int j = 0, i = 0; i < drawPoints.length; ++i){
			//Adds a z float of 0 and adds one additional point (x,y,z)
			//to same x but y=0 to make line into triangle to be drawn
			mMapMaskVerts[j] = drawPoints[i].getX();
			mMapMaskVerts[++j] = 0;
			mMapMaskVerts[++j] = 0;//z
			++j;

			mMapMaskVerts[j] = drawPoints[i].getX();
			mMapMaskVerts[++j] = drawPoints[i].getY();
			mMapMaskVerts[++j] = 0;//z
			++j;
		}
		mMaskVertBuffer = VertexBuffer.getVertexBuffer(mMapMaskVerts);

		mWorldVertBuffer = VertexBuffer.getVertexBuffer(new float[]{
				0,0,0,
				0,GameConVars.MAP_WORLD_HEIGHT,0,
				getSize(),0,0,
				getSize(),GameConVars.MAP_WORLD_HEIGHT,0
		});

		mDirtTextureBuffer = VertexBuffer.getVertexBuffer(new float[]{
				0, GameConVars.MAP_WORLD_HEIGHT/mDirtTextureScale,
				0, 0,
				getSize()/mDirtTextureScale, GameConVars.MAP_WORLD_HEIGHT/mDirtTextureScale,
				getSize()/mDirtTextureScale, 0
		});

		float grass[] = new float[drawPoints.length*3];
		for(int j = 0, i = 0; i < drawPoints.length; ++i,++j){
			grass[j] = drawPoints[i].getX();
			grass[++j] = drawPoints[i].getY();
			grass[++j] = 0;
		}

		mGrassVertBuffer = VertexBuffer.getVertexBuffer(grass);
	}
	
	
	/**
	 * 
	 * Freeze all heightMap points that are within a specified x value range.
	 * 
	 * @param xLoc Center point of the area.
	 * @param width Width around the center point to un-freeze.
	 * 
	 */
	private void freezeMapPoints(float xLoc, int width) { 
		for(PointFFlagged p : heightMap) if(p.x >= xLoc - 0.5 * width && p.x <= xLoc + 0.5 * width) p.frozen = true;
	}
	
	/**
	 * 
	 * Un-freeze all heightMap points that are within a specified x value range.
	 * 
	 * @param xLoc Center point of the area.
	 * @param width With around the center point to un-freeze.
	 * 
	 */
	public void structureDestroyedAtX(float xLoc, int width) {
		for(PointFFlagged p : heightMap) if(p.x >= xLoc - 0.5 * width && p.x <= xLoc + 0.5 * width) p.frozen = false;		
	}
	
	//Getters and setters.
	public PointF[] getTurretLocations() { return turretLocations; }
	public PointF[] getResourceLocations() { return resourceLocations; }
	public PointF[] getBaseLocations() { return baseLocations; }
	public PointFFlagged[] getMap() { return drawPoints; }
	public PointFFlagged[] getHeightMap() { return heightMap; }
	public float getSize(){	return size; }
	public int getTurretCount() { return turretCount; }
	public int getResourceCount() { return resourceCount; }
	public int getTileSet() { return tileset; }
	
	
}